'use strict'

const { LRUCache } = require('lru-cache')
const vulnerabilitiesFormatter = require('./vulnerabilities-formatter')
const { IAST_ENABLED_TAG_KEY, IAST_JSON_TAG_KEY } = require('./tags')
const { keepTrace } = require('../../priority_sampler')
const { reportStackTrace, getCallsiteFrames, canReportStackTrace, STACK_TRACE_NAMESPACES } = require('../stack_trace')
const { getOriginalPathAndLineFromSourceMap } = require('./taint-tracking/rewriter')
const { ASM } = require('../../standalone/product')

const VULNERABILITIES_KEY = 'vulnerabilities'
const VULNERABILITY_HASHES_MAX_SIZE = 1000
const VULNERABILITY_HASHES = new LRUCache({ max: VULNERABILITY_HASHES_MAX_SIZE })
const RESET_VULNERABILITY_CACHE_INTERVAL = 60 * 60 * 1000 // 1 hour

let tracer
let resetVulnerabilityCacheTimer
let deduplicationEnabled = true
let stackTraceEnabled = true
let stackTraceMaxDepth
let maxStackTraces

function canAddVulnerability (vulnerability) {
  const hasRequiredFields = vulnerability?.evidence && vulnerability?.type && vulnerability?.location
  if (!hasRequiredFields) return false

  const isDuplicated = deduplicationEnabled && isDuplicatedVulnerability(vulnerability)

  return !isDuplicated
}

function addVulnerability (iastContext, vulnerability, callSiteFrames) {
  if (!canAddVulnerability(vulnerability)) return

  VULNERABILITY_HASHES.set(`${vulnerability.type}${vulnerability.hash}`, true)

  let span = iastContext?.rootSpan

  if (!span && tracer) {
    span = tracer.startSpan('vulnerability', {
      type: 'vulnerability'
    })

    vulnerability.location.spanId = span.context().toSpanId()

    span.addTags({
      [IAST_ENABLED_TAG_KEY]: 1
    })
  }

  if (!span) return

  keepTrace(span, ASM)

  if (stackTraceEnabled && canReportStackTrace(span, maxStackTraces, STACK_TRACE_NAMESPACES.IAST)) {
    const originalCallSiteList = callSiteFrames.map(callsite => replaceCallSiteFromSourceMap(callsite))

    reportStackTrace(
      span,
      vulnerability.location.stackId,
      originalCallSiteList,
      STACK_TRACE_NAMESPACES.IAST
    )
  }

  if (iastContext?.rootSpan) {
    iastContext[VULNERABILITIES_KEY] = iastContext[VULNERABILITIES_KEY] || []
    iastContext[VULNERABILITIES_KEY].push(vulnerability)
  } else {
    sendVulnerabilities([vulnerability], span)
    span.finish()
  }
}

function isValidVulnerability (vulnerability) {
  return vulnerability && vulnerability.type &&
    vulnerability.evidence &&
    vulnerability.location && vulnerability.location.spanId
}

function sendVulnerabilities (vulnerabilities, span) {
  if (vulnerabilities?.length && span?.addTags) {
    const validatedVulnerabilities = vulnerabilities.filter(isValidVulnerability)
    const jsonToSend = vulnerabilitiesFormatter.toJson(validatedVulnerabilities)

    if (jsonToSend.vulnerabilities.length > 0) {
      const tags = {
        // TODO: Store this outside of the span and set the tag in the exporter.
        [IAST_JSON_TAG_KEY]: JSON.stringify(jsonToSend)
      }
      span.addTags(tags)
    }
  }
  return IAST_JSON_TAG_KEY
}

function clearCache () { // only for test purposes
  VULNERABILITY_HASHES.clear()
}

function startClearCacheTimer () {
  resetVulnerabilityCacheTimer = setInterval(clearCache, RESET_VULNERABILITY_CACHE_INTERVAL)
  resetVulnerabilityCacheTimer.unref()
}

function stopClearCacheTimer () {
  if (resetVulnerabilityCacheTimer) {
    clearInterval(resetVulnerabilityCacheTimer)
    resetVulnerabilityCacheTimer = null
  }
}

function isDuplicatedVulnerability (vulnerability) {
  return VULNERABILITY_HASHES.get(`${vulnerability.type}${vulnerability.hash}`)
}

function getVulnerabilityCallSiteFrames () {
  return getCallsiteFrames(stackTraceMaxDepth, getVulnerabilityCallSiteFrames)
}

function replaceCallSiteFromSourceMap (callsite) {
  if (callsite) {
    const { path, line, column } = getOriginalPathAndLineFromSourceMap(callsite)
    if (path) {
      callsite.file = path
      callsite.path = path
    }
    if (line) {
      callsite.line = line
    }
    // We send the column in the stack trace but not in the vulnerability location
    if (column) {
      callsite.column = column
    }
  }

  return callsite
}

function start (config, _tracer) {
  deduplicationEnabled = config.iast.deduplicationEnabled
  stackTraceEnabled = config.iast.stackTrace.enabled
  stackTraceMaxDepth = config.appsec.stackTrace.maxDepth
  maxStackTraces = config.appsec.stackTrace.maxStackTraces

  vulnerabilitiesFormatter.setRedactVulnerabilities(
    config.iast.redactionEnabled,
    config.iast.redactionNamePattern,
    config.iast.redactionValuePattern
  )
  if (deduplicationEnabled) {
    startClearCacheTimer()
  }
  tracer = _tracer
}

function stop () {
  stopClearCacheTimer()
}

module.exports = {
  addVulnerability,
  sendVulnerabilities,
  getVulnerabilityCallSiteFrames,
  replaceCallSiteFromSourceMap,
  clearCache,
  start,
  stop
}
